/**
 * Software License, Version 1.0
 * 
 * Copyright 2003 The Trustees of Indiana University.  All rights reserved.
 * 
 *
 *Redistribution and use in source and binary forms, with or without 
 *modification, are permitted provided that the following conditions are met:
 *
 *1) All redistributions of source code must retain the above copyright notice,
 * the list of authors in the original source code, this list of conditions and
 * the disclaimer listed in this license;
 *2) All redistributions in binary form must reproduce the above copyright 
 * notice, this list of conditions and the disclaimer listed in this license in
 * the documentation and/or other materials provided with the distribution;
 *3) Any documentation included with all redistributions must include the 
 * following acknowledgement:
 *
 *"This product includes software developed by the Community Grids Lab. For 
 * further information contact the Community Grids Lab at 
 * http://communitygrids.iu.edu/."
 *
 * Alternatively, this acknowledgement may appear in the software itself, and 
 * wherever such third-party acknowledgments normally appear.
 * 
 *4) The name Indiana University or Community Grids Lab or NaradaBrokering, 
 * shall not be used to endorse or promote products derived from this software 
 * without prior written permission from Indiana University.  For written 
 * permission, please contact the Advanced Research and Technology Institute 
 * ("ARTI") at 351 West 10th Street, Indianapolis, Indiana 46202.
 *5) Products derived from this software may not be called NaradaBrokering, 
 * nor may Indiana University or Community Grids Lab or NaradaBrokering appear
 * in their name, without prior written permission of ARTI.
 * 
 *
 * Indiana University provides no reassurances that the source code provided 
 * does not infringe the patent or any other intellectual property rights of 
 * any other entity.  Indiana University disclaims any liability to any 
 * recipient for claims brought by any other entity based on infringement of 
 * intellectual property rights or otherwise.  
 *
 *LICENSEE UNDERSTANDS THAT SOFTWARE IS PROVIDED "AS IS" FOR WHICH NO 
 *WARRANTIES AS TO CAPABILITIES OR ACCURACY ARE MADE. INDIANA UNIVERSITY GIVES
 *NO WARRANTIES AND MAKES NO REPRESENTATION THAT SOFTWARE IS FREE OF 
 *INFRINGEMENT OF THIRD PARTY PATENT, COPYRIGHT, OR OTHER PROPRIETARY RIGHTS. 
 *INDIANA UNIVERSITY MAKES NO WARRANTIES THAT SOFTWARE IS FREE FROM "BUGS", 
 *"VIRUSES", "TROJAN HORSES", "TRAP DOORS", "WORMS", OR OTHER HARMFUL CODE.  
 *LICENSEE ASSUMES THE ENTIRE RISK AS TO THE PERFORMANCE OF SOFTWARE AND/OR 
 *ASSOCIATED MATERIALS, AND TO THE PERFORMANCE AND VALIDITY OF INFORMATION 
 *GENERATED USING SOFTWARE.
 */
package cgl.narada.jms;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.Enumeration;
import java.util.Hashtable;

import javax.jms.JMSException;
import javax.jms.MapMessage;
import javax.jms.MessageFormatException;
import javax.jms.MessageNotWriteableException;

import cgl.narada.util.ObjectMarshaller;

/** This class implements the MapMessage specified in the JMS specification.
    @author Shrideep Pallickara
    $Date$
    $Revision$
*/
 
public class JmsMapMessage extends JmsMessage implements MapMessage {
  private Hashtable mappedEntries;
  private String moduleName = "JmsMapMessage: ";
  
  public JmsMapMessage() throws JMSException {
    mappedEntries = new Hashtable();
    setJMSType("MapMessage");
  }
  
  public JmsMapMessage(byte[] messageBytes) throws JMSException {
    ByteArrayInputStream baInputStream = 
      new ByteArrayInputStream(messageBytes);
    DataInputStream din = new DataInputStream(baInputStream);
    
    try {
      int headerAndPropertyLength = din.readInt(); 
      byte[] headerPropBytes = new byte[headerAndPropertyLength];
      din.readFully(headerPropBytes);
      super.unmarshallJmsMessage(headerPropBytes);
      
      /** Proceed to unmarshall the bytes */
      messageBytes = new byte[din.readInt()];
      din.readFully(messageBytes);
      
      mappedEntries = (Hashtable) ObjectMarshaller.unmarshall(messageBytes);
      setJMSType("MapMessage");
    } catch (Exception e) {
      throw new JMSException(moduleName + "Error while un-marshalling" + e);
    }/* end try-catch */
    
  }


  public byte[] getBytes() {
     ByteArrayOutputStream baOutputStream = new ByteArrayOutputStream();
    DataOutputStream dout = new DataOutputStream(baOutputStream);
    byte[] marshalledBytes = null;
    try {
      byte[] headerPropBytes = super.getBytes();
      /* Write the length of the byte Stream comprising headers and properties
	 first.*/
      dout.writeInt(headerPropBytes.length);
      dout.write(headerPropBytes);
      
      /** Proceed to marshall the byte array */
      byte[] messageBytes = ObjectMarshaller.marshall(mappedEntries);
      dout.writeInt(messageBytes.length);
      dout.write(messageBytes);
      
      dout.flush();
      marshalledBytes = baOutputStream.toByteArray();
    } catch (IOException ioe) {
      System.out.println(moduleName + "Error marshalling ..." + ioe);
    }/* end try-catch block */
    return marshalledBytes;
  }

  private void checkPriorToEntryRetrieval(String name) 
    throws JMSException {
    if (mappedEntries == null) {
      throw new JMSException(moduleName + "Underlying mapped entry is null!");
    }
    
    if (!mappedEntries.containsKey(name)) {
      throw new JMSException(moduleName + "Unknown map name [" + name + 
			     "] specified"); 
    }
  }


  private void 
  handleInvalidTypeConversion(Object object, String conversionTo)
    throws JMSException {
    throw new JMSException(moduleName + "Attempting type conversion of " +
			   " primitive in [" + object.getClass() + 
			   "] to (" + conversionTo +  ") is an invalid one!");
  }

  
  private void checkIfEntryAlreadyPresent(String name) 
    throws JMSException {
    if (mappedEntries == null) {
      throw new JMSException(moduleName + "Underlying mapped entry is null!");
    }
    
    if (mappedEntries.containsKey(name)) {
      Object object = mappedEntries.get(name);
      throw new JMSException(moduleName + "Map name [" + name + 
			     "] already specified. The corresponding value " +
			     "is of type [" + object.getClass() + 
			     "] with value =>" + object); 
    }
  }

  /** Returns the <CODE>boolean</CODE> value with the specified name.
   *
   * @param name the name of the <CODE>boolean</CODE>
   *
   * @return the <CODE>boolean</CODE> value with the specified name
   *
   * @exception JMSException if the JMS provider fails to read the message
   *                         due to some internal error.
   * @exception MessageFormatException if this type conversion is invalid.
   */
  
  public boolean getBoolean(String name) throws JMSException {
    checkPriorToEntryRetrieval(name);
   
    Object object = mappedEntries.get(name);
    
    if (object instanceof Boolean) {
      return ((Boolean)object).booleanValue();
    }
    
    if (object instanceof String) {
      return Boolean.valueOf((String)object).booleanValue();
    }
    
    handleInvalidTypeConversion(object, "boolean");
    return false;
  }
  
  
  /** Returns the <CODE>byte</CODE> value with the specified name.
   *
   * @param name the name of the <CODE>byte</CODE>
   *
   * @return the <CODE>byte</CODE> value with the specified name
   *
   * @exception JMSException if the JMS provider fails to read the message
   *                         due to some internal error.
   * @exception MessageFormatException if this type conversion is invalid.
   */ 
  public byte getByte(String name) throws JMSException {
    checkPriorToEntryRetrieval(name);
    
    Object object = mappedEntries.get(name);
    
    if (object instanceof Byte) {
      return ((Byte)object).byteValue();
    }
    
    if (object instanceof String) {
      return Byte.valueOf((String)object).byteValue();
    }

    handleInvalidTypeConversion(object, "byte");
    return 0;
  }
  
  
  /** Returns the <CODE>short</CODE> value with the specified name.
   *
   * @param name the name of the <CODE>short</CODE>
   *
   * @return the <CODE>short</CODE> value with the specified name
   *
   * @exception JMSException if the JMS provider fails to read the message
   *                         due to some internal error.
   * @exception MessageFormatException if this type conversion is invalid.
   */ 
  public short getShort(String name) throws JMSException {
    checkPriorToEntryRetrieval(name);
    
    Object object = mappedEntries.get(name);
    
    if (object instanceof Short || object instanceof Byte) {      
      return ( (Number)object).shortValue();
    }
    
    if (object instanceof String) {
      return Short.valueOf((String)object).shortValue();
    }
    
    handleInvalidTypeConversion(object, "short");
    return 0;
  }
  
  
  /** Returns the Unicode character value with the specified name.
   *
   * @param name the name of the Unicode character
   *
   * @return the Unicode character value with the specified name
   *
   * @exception JMSException if the JMS provider fails to read the message
   *                         due to some internal error.
   * @exception MessageFormatException if this type conversion is invalid.     
   */ 
  public char getChar(String name) throws JMSException {
    checkPriorToEntryRetrieval(name);
    Object object = mappedEntries.get(name);
    
    if (object instanceof Character) {
      return  ((Character) object).charValue();
    }
    
    handleInvalidTypeConversion(object, "char");
    return 0;
  }
  
  
  /** Returns the <CODE>int</CODE> value with the specified name.
   *
   * @param name the name of the <CODE>int</CODE>
   *
   * @return the <CODE>int</CODE> value with the specified name
   *
   * @exception JMSException if the JMS provider fails to read the message
   *                         due to some internal error.
   * @exception MessageFormatException if this type conversion is invalid.     
   */ 
  public int getInt(String name) throws JMSException {
    checkPriorToEntryRetrieval(name);
    
    Object object = mappedEntries.get(name);
    if (object instanceof Integer || object instanceof Short 
	|| object instanceof Byte) {      
      return ( (Number)object).intValue();
    }

    if (object instanceof String) {
      return Integer.valueOf((String)object).intValue();
    }
    
    handleInvalidTypeConversion(object, "int");
    return 0;
  }
  
  
  /** Returns the <CODE>long</CODE> value with the specified name.
   *
   * @param name the name of the <CODE>long</CODE>
   *
   * @return the <CODE>long</CODE> value with the specified name
   *
   * @exception JMSException if the JMS provider fails to read the message
   *                         due to some internal error.
   * @exception MessageFormatException if this type conversion is invalid.     
   */ 
  public long getLong(String name) throws JMSException {
    checkPriorToEntryRetrieval(name);
    
    Object object = mappedEntries.get(name);
    
    if (object instanceof Long || object instanceof Integer ||
	object instanceof Short || object instanceof Byte) {      
      return ( (Number)object).longValue();
    }

    
    if (object instanceof String) {
      return Long.valueOf((String)object).longValue();
    }
    
    handleInvalidTypeConversion(object, "long");
    return 0;
  }
  
  
  /** Returns the <CODE>float</CODE> value with the specified name.
   *
   * @param name the name of the <CODE>float</CODE>
   *
   * @return the <CODE>float</CODE> value with the specified name
   *
   * @exception JMSException if the JMS provider fails to read the message
   *                         due to some internal error.
   * @exception MessageFormatException if this type conversion is invalid.     
   */ 
  public float getFloat(String name) throws JMSException {
    checkPriorToEntryRetrieval(name);
    
    Object object = mappedEntries.get(name);
    
    if (object instanceof Float) {
      return ( (Float)object).floatValue();
    }
    
    if (object instanceof String) {
      return Float.valueOf((String)object).floatValue();
    }
    
    handleInvalidTypeConversion(object, "float");
    return 0;
  }
  
  
  /** Returns the <CODE>double</CODE> value with the specified name.
   *
   * @param name the name of the <CODE>double</CODE>
   *
   * @return the <CODE>double</CODE> value with the specified name
   *
   * @exception JMSException if the JMS provider fails to read the message
   *                         due to some internal error.
   * @exception MessageFormatException if this type conversion is invalid.     
   */ 
  public double getDouble(String name) throws JMSException {
    checkPriorToEntryRetrieval(name);
    
    Object object = mappedEntries.get(name);
    if (object instanceof Double) {
      return ( (Double)object).doubleValue();
    }
    
    if (object instanceof Float) {
      return ( (Float)object).doubleValue();
    }
    
    if (object instanceof String) {
      return Float.valueOf((String)object).doubleValue();
    }
    
    handleInvalidTypeConversion(object, "double");
    return 0;
  } 
  
  
  /** Returns the <CODE>String</CODE> value with the specified name.
   *
   * @param name the name of the <CODE>String</CODE>
   *
   * @return the <CODE>String</CODE> value with the specified name; if there 
   * is no item by this name, a null value is returned
   *
   * @exception JMSException if the JMS provider fails to read the message
   *                         due to some internal error.
   * @exception MessageFormatException if this type conversion is invalid.     
   */ 
  public String getString(String name) throws JMSException {
    checkPriorToEntryRetrieval(name);
    Object object = mappedEntries.get(name);
    
    if (object instanceof Boolean || object instanceof Number || 
	object instanceof Character ||
	object instanceof Float || object instanceof Double ||
	object instanceof String) {
      return object.toString(); 
    }
    
    handleInvalidTypeConversion(object, "String");
    return null;
  }
  
  
  /** Returns the byte array value with the specified name.
   *
   * @param name the name of the byte array
   *
   * @return a copy of the byte array value with the specified name; if there
   * is no
   * item by this name, a null value is returned.
   *
   * @exception JMSException if the JMS provider fails to read the message
   *                         due to some internal error.
   * @exception MessageFormatException if this type conversion is invalid.     
   */ 
  
  public byte[] getBytes(String name) throws JMSException {
    checkPriorToEntryRetrieval(name);
    Object object = mappedEntries.get(name);

    if (object instanceof byte[]) {
      return (byte[])object;
    }
    handleInvalidTypeConversion(object, "byte[]");
     return null;
  }
  
  
  /** Returns the value of the object with the specified name.
   *
   * <P>This method can be used to return, in objectified format,
   * an object in the Java programming language ("Java object") that had 
   * been stored in the Map with the equivalent
   * <CODE>setObject</CODE> method call, or its equivalent primitive
   * <CODE>set<I>type</I></CODE> method.
   *
   * <P>Note that byte values are returned as <CODE>byte[]</CODE>, not 
   * <CODE>Byte[]</CODE>.
   *
   * @param name the name of the Java object
   *
   * @return a copy of the Java object value with the specified name, in 
   * objectified format (for example, if the object was set as an 
   * <CODE>int</CODE>, an <CODE>Integer</CODE> is returned); if there is no 
   * item by this name, a null value is returned
   *
   * @exception JMSException if the JMS provider fails to read the message
   *                         due to some internal error.
   */ 
  public Object 
  getObject(String name) throws JMSException {
    checkPriorToEntryRetrieval(name);
    Object object = mappedEntries.get(name);
    
    if (object instanceof Boolean || object instanceof Number || 
	object instanceof Character ||
	object instanceof Float || object instanceof Double ||
	object instanceof String || object instanceof byte[]) {
      return object; 
    }
    
    handleInvalidTypeConversion(object, "Object");
    return null;
  }
  
  
  
  /** Returns an <CODE>Enumeration</CODE> of all the names in the 
   * <CODE>MapMessage</CODE> object.
   *
   * @return an enumeration of all the names in this <CODE>MapMessage</CODE>
   *
   * @exception JMSException if the JMS provider fails to read the message
   *                         due to some internal error.
   */
  
  public Enumeration getMapNames() throws JMSException {
    if (mappedEntries == null) {
      throw new JMSException(moduleName + 
			     "Underlying mapped entries are null");
    }

    return mappedEntries.keys();
  }


  /** Sets a <CODE>boolean</CODE> value with the specified name into the Map.
   *
   * @param name the name of the <CODE>boolean</CODE>
   * @param value the <CODE>boolean</CODE> value to set in the Map
   *
   * @exception JMSException if the JMS provider fails to write the message
   *                         due to some internal error.
   * @exception MessageNotWriteableException if the message is in read-only 
   *                                         mode.
   */
  public void setBoolean(String name, boolean value) throws JMSException {
    checkIfEntryAlreadyPresent(name);
    mappedEntries.put(name, new Boolean(value));
  }
  
  
  /** Sets a <CODE>byte</CODE> value with the specified name into the Map.
   *
   * @param name the name of the <CODE>byte</CODE>
   * @param value the <CODE>byte</CODE> value to set in the Map
   *
   * @exception JMSException if the JMS provider fails to write the message
   *                         due to some internal error.
   * @exception MessageNotWriteableException if the message is in read-only 
   *                                         mode.
   */ 
  public void setByte(String name, byte value) throws JMSException {
     checkIfEntryAlreadyPresent(name);
    mappedEntries.put(name, new Byte(value));
  }
  
  
  /** Sets a <CODE>short</CODE> value with the specified name into the Map.
   *
   * @param name the name of the <CODE>short</CODE>
   * @param value the <CODE>short</CODE> value to set in the Map
   *
   * @exception JMSException if the JMS provider fails to write the message
   *                         due to some internal error.
   * @exception MessageNotWriteableException if the message is in read-only 
   *                                         mode.
   */ 
  public void setShort(String name, short value) throws JMSException {
    checkIfEntryAlreadyPresent(name);
    mappedEntries.put(name, new Short(value));
  }
  
  
  /** Sets a Unicode character value with the specified name into the Map.
   *
   * @param name the name of the Unicode character
   * @param value the Unicode character value to set in the Map
   *
   * @exception JMSException if the JMS provider fails to write the message
   *                         due to some internal error.
   * @exception MessageNotWriteableException if the message is in read-only 
   *                                         mode.
   */ 
  public void setChar(String name, char value) throws JMSException {
    checkIfEntryAlreadyPresent(name);
    mappedEntries.put(name, new Character(value)); 
  }
  
  
  /** Sets an <CODE>int</CODE> value with the specified name into the Map.
   *
   * @param name the name of the <CODE>int</CODE>
   * @param value the <CODE>int</CODE> value to set in the Map
   *
   * @exception JMSException if the JMS provider fails to write the message
   *                         due to some internal error.
   * @exception MessageNotWriteableException if the message is in read-only 
   *                                         mode.
   */ 
  public void setInt(String name, int value) throws JMSException {
    checkIfEntryAlreadyPresent(name);
    mappedEntries.put(name, new Integer(value));
  }
  
  
  /** Sets a <CODE>long</CODE> value with the specified name into the Map.
   *
   * @param name the name of the <CODE>long</CODE>
   * @param value the <CODE>long</CODE> value to set in the Map
   *
   * @exception JMSException if the JMS provider fails to write the message
   *                         due to some internal error.
   * @exception MessageNotWriteableException if the message is in read-only 
   *                                         mode.
   */ 
  public  void  setLong(String name, long value) throws JMSException {
    checkIfEntryAlreadyPresent(name);
    mappedEntries.put(name, new Long(value));
  }
  
  /** Sets a <CODE>float</CODE> value with the specified name into the Map.
   *
   * @param name the name of the <CODE>float</CODE>
   * @param value the <CODE>float</CODE> value to set in the Map
   *
   * @exception JMSException if the JMS provider fails to write the message
   *                         due to some internal error.
   * @exception MessageNotWriteableException if the message is in read-only 
   *                                         mode.
   */ 
  public void setFloat(String name, float value) throws JMSException {
    checkIfEntryAlreadyPresent(name);
    mappedEntries.put(name, new Float(value));
  }
  
  
  /** Sets a <CODE>double</CODE> value with the specified name into the Map.
   *
   * @param name the name of the <CODE>double</CODE>
   * @param value the <CODE>double</CODE> value to set in the Map
   *
   * @exception JMSException if the JMS provider fails to write the message
   *                         due to some internal error.
   * @exception MessageNotWriteableException if the message is in read-only 
   *                                         mode.
   */ 
  public void setDouble(String name, double value) throws JMSException {
    checkIfEntryAlreadyPresent(name);
    mappedEntries.put(name, new Double(value));
  }


  /** Sets a <CODE>String</CODE> value with the specified name into the Map.
   *
   * @param name the name of the <CODE>String</CODE>
   * @param value the <CODE>String</CODE> value to set in the Map
   *
   * @exception JMSException if the JMS provider fails to write the message
   *                         due to some internal error.
   * @exception MessageNotWriteableException if the message is in read-only 
   *                                         mode.
   */ 
  public void setString(String name, String value) throws JMSException {
    checkIfEntryAlreadyPresent(name);
    if (value == null) {
      throw new JMSException(moduleName + "Specified value for map [" +
			     name + "] is NULL");
    }
    mappedEntries.put(name, value);
  }
  
  
  /** Sets a byte array value with the specified name into the Map.
   *
   * @param name the name of the byte array
   * @param value the byte array value to set in the Map; the array
   *              is copied so that the value for <CODE>name</CODE> will
   *              not be altered by future modifications
   *
   * @exception JMSException if the JMS provider fails to write the message
   *                         due to some internal error.
   * @exception MessageNotWriteableException if the message is in read-only 
   *                                         mode.
   */ 
  public void setBytes(String name, byte[] value) throws JMSException {
    checkIfEntryAlreadyPresent(name);    
    if (value == null) {
      throw new JMSException(moduleName + "Specified value for map [" +
			     name + "] is NULL");
    }
    
    mappedEntries.put(name, value);
  }
  
  /** Sets a portion of the byte array value with the specified name into the 
   * Map.
   *  
   * @param name the name of the byte array
   * @param value the byte array value to set in the Map
   * @param offset the initial offset within the byte array
   * @param length the number of bytes to use
   *
   * @exception JMSException if the JMS provider fails to write the message
   *                         due to some internal error.
   * @exception MessageNotWriteableException if the message is in read-only 
   *                                         mode.
   */ 
  public void setBytes(String name, byte[] value, int offset, int length) 
    throws JMSException  {
    checkIfEntryAlreadyPresent(name);    
    if (value == null) {
      throw new JMSException(moduleName + "Specified value for map [" +
			     name + "] is NULL");
    }

    byte[] newvalue = new byte[length];
    System.arraycopy(value, offset, newvalue, 0, length);
    mappedEntries.put(name, newvalue);
    
  }
  
  
  /** Sets an object value with the specified name into the Map.
   *
   * <P>This method works only for the objectified primitive
   * object types (<code>Integer</code>, <code>Double</code>, 
   * <code>Long</code>&nbsp;...), <code>String</code> objects, and byte 
   * arrays.
   *
   * @param name the name of the Java object
   * @param value the Java object value to set in the Map
   *
   * @exception JMSException if the JMS provider fails to write the message
   *                         due to some internal error.
   * @exception MessageFormatException if the object is invalid.
   * @exception MessageNotWriteableException if the message is in read-only 
   *                                         mode.
   */ 
  public void setObject(String name, Object value) throws JMSException {
    checkIfEntryAlreadyPresent(name);    
    if (value instanceof Boolean || value instanceof Number || 
	value instanceof Character ||
	value instanceof Float || value instanceof Double ||
	value instanceof String || value instanceof byte[]) {
      mappedEntries.put(name, value);
    }
    
    throw new JMSException(moduleName + "Invalid Object of type=[" +
			   value.getClass() +"] and value=[" + value + 
			   "] specified.");
  }
  
  
  /** Indicates whether an item exists in this <CODE>MapMessage</CODE> object.
   *
   * @param name the name of the item to test
   *
   * @return true if the item exists
   *
   * @exception JMSException if the JMS provider fails to determine if the 
   *                         item exists due to some internal error.
   */ 
  public boolean  itemExists(String name) throws JMSException {
    
    if (mappedEntries == null) {
      throw new JMSException(moduleName + "Underlying mapped entries = null");
    }

    if (mappedEntries.containsKey(name)) {
      return true;
    }

    return false;
  }


  public static void main(String[] args) {
    try {
      JmsMapMessage map1 = new JmsMapMessage();
      map1.setInt("bob", 111);
      map1.setLong("dylan", 202);
      short sval = 497;
      map1.setString("jordan", new Short(sval).toString());
      map1.setShort("mike", sval );
      byte[] mapBytes = map1.getBytes();
      JmsMapMessage map2 = new JmsMapMessage(mapBytes);
      System.out.println("map2.getLong(bob) => " + map2.getLong("bob") );
      System.out.println("map2.getLong(dylan) => " + map2.getLong("dylan") );
      
      System.out.println("map2.getLong(jordan) =>" + map2.getLong("jordan") );
      System.out.println("map2.getInt(mike) =>" + map2.getInt("mike") );
      System.out.println("map2.getInt(dylan) =>" + map2.getInt("dylan") );
    } catch (JMSException jmsEx) {
      System.out.println(jmsEx);
    }
  }

}
